/*******************************************************************************
 * Copyright (c) 2002, 2016 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Rational Software - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.core.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementDelta;
import org.eclipse.cdt.core.model.IParent;

/**
 * A C element delta builder creates a C element delta on a C element between
 * the version of the C element at the time the comparator was created and the
 * current version of the C element.
 *
 * It performs this operation by locally caching the contents of 
 * the C element when it is created. When the method buildDeltas() is called, it
 * creates a delta over the cached contents and the new contents.
 * 
 * This class is similar to the JDT CElementDeltaBuilder class.
 */

public class CElementDeltaBuilder {
	
	/**
	 * The c element handle
	 */
	ICElement cElement;

	/**
	 * The maximum depth in the C element children we should look into
	 */
	int maxDepth = Integer.MAX_VALUE;

	/**
	 * The old handle to info relationships
	 */
	Map<ICElement, CElementInfo> infos;

	/**
	 * The old position info
	 */
	Map<ICElement, ListItem> oldPositions;

	/**
	 * The new position info
	 */
	Map<ICElement, ListItem> newPositions;

	/**
	 * Change delta
	 */
	CElementDelta delta;

	/**
	 * List of added elements
	 */
	ArrayList<ICElement> added;

	/**
	 * List of removed elements
	 */
	ArrayList<ICElement> removed;
	
	/**
	 * Doubly linked list item
	 */
	class ListItem {
		public ICElement previous;
		public ICElement next;

		public ListItem(ICElement previous, ICElement next) {
			this.previous = previous;
			this.next = next;
		}
	}
/**
 * Creates a C element comparator on a C element
 * looking as deep as necessary.
 */
public CElementDeltaBuilder(ICElement CElement) {
	this.cElement = CElement;
	this.initialize();
	this.recordElementInfo(CElement,0);
}
/**
 * Creates a C element comparator on a C element
 * looking only 'maxDepth' levels deep.
 */
public CElementDeltaBuilder(ICElement cElement, int maxDepth) {
	this.cElement = cElement;
	this.maxDepth = maxDepth;
	this.initialize();
	this.recordElementInfo(cElement,0);
}
/**
 * Repairs the positioning information
 * after an element has been added
 */
private void added(ICElement element) {
	this.added.add(element);
	ListItem current = this.getNewPosition(element);
	ListItem previous = null, next = null;
	if (current.previous != null)
		previous = this.getNewPosition(current.previous);
	if (current.next != null)
		next = this.getNewPosition(current.next);
	if (previous != null)
		previous.next = current.next;
	if (next != null)
		next.previous = current.previous;
}
/**
 * Builds the C element deltas between the old content of the translation unit
 * and its new content.
 */
public void buildDeltas() throws CModelException {
	this.recordNewPositions(this.cElement, 0);
	this.findAdditions(this.cElement, 0);
	this.findDeletions();
	this.findChangesInPositioning(this.cElement, 0);
	this.trimDelta(this.delta);
}
/**
 * Finds elements which have been added or changed.
 */
private void findAdditions(ICElement newElement, int depth) throws CModelException {
	CElementInfo oldInfo = this.getElementInfo(newElement);
	if (oldInfo == null && depth < this.maxDepth) {
		this.delta.added(newElement);
		added(newElement);
	} else {
		this.removeElementInfo(newElement);
	}
	
	if (depth >= this.maxDepth) {
		// mark element as changed
		this.delta.changed(newElement, ICElementDelta.F_CONTENT);
		return;
	}

	CElementInfo newInfo = null;
	newInfo = ((CElement)newElement).getElementInfo();
	
	this.findContentChange(oldInfo, newInfo, newElement);
		
	if (oldInfo != null && newElement instanceof IParent) {

		ICElement[] children = newInfo.getChildren();
		if (children != null) {
			int length = children.length;
			for(int i = 0; i < length; i++) {
				this.findAdditions(children[i], depth + 1);
			}
		}		
	}
}
/**
 * Looks for changed positioning of elements.
 */
private void findChangesInPositioning(ICElement element, int depth) throws CModelException {
	if (depth >= this.maxDepth || this.added.contains(element) || this.removed.contains(element))
		return;
		
	if (!isPositionedCorrectly(element)) {
		this.delta.changed(element, ICElementDelta.F_REORDER);
	} 
	
	if (element instanceof IParent) {
		CElementInfo info = null;
		info = ((CElement)element).getElementInfo();

		ICElement[] children = info.getChildren();
		if (children != null) {
			int length = children.length;
			for(int i = 0; i < length; i++) {
				this.findChangesInPositioning(children[i], depth + 1);
			}
		}		
	}
}
/**
 * The elements are equivalent, but might have content changes.
 */
private void findContentChange(CElementInfo oldInfo, CElementInfo newInfo, ICElement newElement) {
	if (oldInfo instanceof SourceManipulationInfo && newInfo instanceof SourceManipulationInfo) {
		SourceManipulationInfo oldSourceInfo = (SourceManipulationInfo) oldInfo;
		SourceManipulationInfo newSourceInfo = (SourceManipulationInfo) newInfo;
		
		if ((oldSourceInfo).getModifiers() != (newSourceInfo).getModifiers()) {
			this.delta.changed(newElement, ICElementDelta.F_MODIFIERS);
		}
		
		// The element info should be able to tell if the contents are the same. 
		if(!oldSourceInfo.hasSameContentsAs(newSourceInfo)){
			this.delta.changed(newElement, ICElementDelta.F_CONTENT);
		}
	}
}
/**
 * Adds removed deltas for any handles left in the table
 */
private void findDeletions() {
	Iterator<ICElement> iter = this.infos.keySet().iterator();
	while(iter.hasNext()) {
		ICElement element = iter.next();
		this.delta.removed(element);
		this.removed(element);
	}
}
private CElementInfo getElementInfo(ICElement element) {
	return this.infos.get(element);
}
private ListItem getNewPosition(ICElement element) {
	return this.newPositions.get(element);
}
private ListItem getOldPosition(ICElement element) {
	return this.oldPositions.get(element);
}
private void initialize() {
	this.infos = new HashMap<ICElement, CElementInfo>(20);
	this.oldPositions = new HashMap<ICElement, ListItem>(20);
	this.newPositions = new HashMap<ICElement, ListItem>(20);
	this.putOldPosition(this.cElement, new ListItem(null, null));
	this.putNewPosition(this.cElement, new ListItem(null, null));
	this.delta = new CElementDelta(cElement);
	
	// if building a delta on a translation unit or below, 
	// it's a fine grained delta
	if (cElement.getElementType() >= ICElement.C_UNIT) {
		this.delta.fineGrained();
	}
	
	this.added = new ArrayList<ICElement>(5);
	this.removed = new ArrayList<ICElement>(5);
}
/**
 * Inserts position information for the elements into the new or old positions table
 */
private void insertPositions(ICElement[] elements, boolean isNew) {
	int length = elements.length;
	ICElement previous = null, current = null, next = (length > 0) ? elements[0] : null;
	for(int i = 0; i < length; i++) {
		previous = current;
		current = next;
		next = (i + 1 < length) ? elements[i + 1] : null;
		if (isNew) {
			this.putNewPosition(current, new ListItem(previous, next));
		} else {
			this.putOldPosition(current, new ListItem(previous, next));
		}
	}
}
/**
 * Returns true if the given elements represent the an equivalent declaration.
 *
 * <p>NOTE: Since this comparison can be done with handle info only,
 * none of the internal calls need to use the locally cached contents
 * of the old translation unit.
 */
//private boolean isIdentical(CElement e1, CElement e2) {
//	if (e1 == null ^ e2 == null)
//		return false;
//	
//	if (e1 == null)
//		return true;
//
//	return e1.isIdentical(e2);					
//}
/**
 * Answers true if the elements position has not changed.
 * Takes into account additions so that elements following
 * new elements will not appear out of place.
 */
private boolean isPositionedCorrectly(ICElement element) {
	ListItem oldListItem = this.getOldPosition(element);
	if (oldListItem == null) return false;
	
	ListItem newListItem = this.getNewPosition(element);
	if (newListItem == null) return false;
	
	ICElement oldPrevious = oldListItem.previous;
	ICElement newPrevious = newListItem.previous;
	
	if (oldPrevious == null) {
		return newPrevious == null;
	}
	return oldPrevious.equals(newPrevious);
}
private void putElementInfo(ICElement element, CElementInfo info) {
	this.infos.put(element, info);
}
private void putNewPosition(ICElement element, ListItem position) {
	this.newPositions.put(element, position);
}
private void putOldPosition(ICElement element, ListItem position) {
	this.oldPositions.put(element, position);
}
/**
 * Records this elements info, and attempts
 * to record the info for the children.
 */
private void recordElementInfo(ICElement element, int depth) {
	if (depth >= this.maxDepth) {
		return;
	}
	CElementInfo info = (CElementInfo)CModelManager.getDefault().getInfo(element);
	if (info == null) // no longer in the C model.
		return;
	this.putElementInfo(element, info);
		
	if (element instanceof IParent) {
		ICElement[] children = info.getChildren();
		if (children != null) {
			insertPositions(children, false);
			for(int i = 0, length = children.length; i < length; i++)
				recordElementInfo(children[i], depth + 1);
		}
	}
}
/**
 * Fills the newPositions hashtable with the new position information
 */
private void recordNewPositions(ICElement newElement, int depth) throws CModelException {
	if (depth < this.maxDepth && newElement instanceof IParent) {
		CElementInfo info = null;
		info = ((CElement)newElement).getElementInfo();

		ICElement[] children = info.getChildren();
		if (children != null) {
			insertPositions(children, true);
			for(int i = 0, length = children.length; i < length; i++) {
				recordNewPositions(children[i], depth + 1);
			}
		}
	}
}
/**
 * Repairs the positioning information
 * after an element has been removed
 */
private void removed(ICElement element) {
	this.removed.add(element);
	ListItem current = this.getOldPosition(element);
	ListItem previous = null, next = null;
	if (current.previous != null)
		previous = this.getOldPosition(current.previous);
	if (current.next != null)
		next = this.getOldPosition(current.next);
	if (previous != null)
		previous.next = current.next;
	if (next != null)
		next.previous = current.previous;
	
}
private void removeElementInfo(ICElement element) {
	this.infos.remove(element);
}
@Override
public String toString() {
	StringBuilder buffer = new StringBuilder();
	buffer.append("Built delta:\n"); //$NON-NLS-1$
	buffer.append(this.delta.toString());
	return buffer.toString();
}
/**
 * Trims deletion deltas to only report the highest level of deletion
 */
private void trimDelta(CElementDelta delta) {
	if (delta.getKind() == ICElementDelta.REMOVED) {
		ICElementDelta[] children = delta.getAffectedChildren();
		for(int i = 0, length = children.length; i < length; i++) {
			delta.removeAffectedChild((CElementDelta)children[i]);
		}
	} else {
		ICElementDelta[] children = delta.getAffectedChildren();
		for(int i = 0, length = children.length; i < length; i++) {
			trimDelta((CElementDelta)children[i]);
		}
	}
}

/**
 * Get the change delta built by the builder.
 * @return the change delta built by the builder.
 */
public CElementDelta getDelta() {
	return delta;
}
	
}
